package turn

import (
	"bufio"
	"container/list"
	"context"
	"encoding/gob"
	"errors"
	"gitee.com/yysf-xin/dproxy/pkg/tools"
	"net"
	"net/http"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"
)

type GroupChooser func(ctx context.Context, network, address string) uint64

func NewServer(c GroupChooser) (s *Server) {
	s = &Server{
		pending:  make(map[uint64]*Call),
		GroupMap: make(map[uint64]*list.List),
		chooser:  c,
	}
	return
}

type Conn struct {
	net.Conn
	call *Call
}

func (c *Conn) Close() error {
	c.call.Close()
	return c.Conn.Close()
}

func (s *Server) DialContext(ctx context.Context, network, address string) (conn net.Conn, err error) {
	c := s.ChooseClient(s.chooser(ctx, network, address))
	if c == nil {
		err = errors.New("client not connect")
		return
	}

	seq := atomic.AddUint64(&s.Seq, 1)
	call := &Call{
		Done: make(chan net.Conn),
	}
	s.mutex.Lock()
	s.pending[seq] = call
	s.mutex.Unlock()

	rpy := ConnectMsg{
		Id:      seq,
		Network: network,
		Addr:    address,
	}
	//tools.Log.Println("client conn", c.Conn.RemoteAddr(), c.GroupId)
	err = c.Encode(ConnectCommand, &rpy)

	if err == nil {
		connCtx, canCel := context.WithTimeout(ctx, 10*time.Second)
		defer canCel()
		tools.Log.Println(turnServerTag, "wait client conn 20s at", c.GroupId)
		select {
		case <-connCtx.Done():
			break

		case conn = <-call.Done:
			break
		}
	} else {
		tools.Log.Println(turnServerTag, err)

	}
	if conn == nil {
		err = errors.New("can not open conn")
		call.Close()
	} else {
		tools.Log.Println(turnServerTag, "got conn")
		conn = &Conn{Conn: conn, call: call}
	}

	s.mutex.Lock()
	delete(s.pending, seq)
	s.mutex.Unlock()

	return
}
func (s *Server) ChooseClient(groupId uint64) (c *ClientCodec) {
	s.groupRw.RLock()
	l := s.GroupMap[groupId]
	if l != nil {
		node := l.Front()
		if node != nil {
			c = node.Value.(*ClientCodec)
		}
	}
	s.groupRw.RUnlock()
	return
}
func (s *Server) addClient(c *ClientCodec) {
	s.groupRw.Lock()
	l := s.GroupMap[c.GroupId]
	if l == nil {
		l = list.New()
		s.GroupMap[c.GroupId] = l
	}
	c.el = l.PushBack(c)
	s.groupRw.Unlock()
}
func (s *Server) removeClient(c *ClientCodec) {
	s.groupRw.Lock()
	l := s.GroupMap[c.GroupId]
	if l != nil {
		l.Remove(c.el)
		if l.Len() == 0 {
			delete(s.GroupMap, c.GroupId)
		}
	}
	c.el = nil
	s.groupRw.Unlock()
}

func readLastId(path string) (id uint64, err error) {
	i := strings.LastIndex(path, "/")
	if i != -1 {
		id, err = strconv.ParseUint(path[i+1:], 10, 64)
	} else {
		err = errors.New("error path " + path)
	}
	return
}

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	protocol := r.Header.Get("Upgrade")
	if protocol != "" && r.Method == http.MethodGet && r.Header.Get("Connection") == "upgrade" {
		var Id uint64
		Id, err := readLastId(r.URL.Path)
		if err != nil {
			http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
			return
		}
		conn, _, err := w.(http.Hijacker).Hijack()
		if err != nil {
			tools.Log.Println(turnServerTag, "rpc hijacking ", r.RemoteAddr, ": ", err.Error())
			http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
			return
		}
		defer conn.Close()
		_, err = conn.Write([]byte("HTTP/1.1 101 Switching Protocols\r\n\r\n"))
		if err != nil {
			return
		}
		if protocol == http.MethodPut {
			encBuf := bufio.NewWriter(conn)
			decBuf := bufio.NewReader(conn)
			codec := &ClientCodec{
				Id,
				conn,
				time.Now().UnixMilli(),
				encBuf,
				gob.NewEncoder(encBuf),
				decBuf,
				gob.NewDecoder(decBuf),
				nil,
				sync.Mutex{},
			}

			s.addClient(codec)
			defer s.removeClient(codec)

			req := &Request{}
			for err == nil {
				if err = codec.dec.Decode(req); err != nil {
					break
				}
				var msg ServerMsgHandler
				switch req.Command {
				case ConnectCommand:
					msg = &ConnectRpy{}
				case PingCommand:
					msg = &PingMsg{}
				}
				if msg != nil {
					err = codec.dec.Decode(msg)
				} else {
					err = errors.New("unknown error")
				}
				if msg != nil && err == nil {
					msg.SeverDo(codec, s)
				}
			}
			if err != nil {
				tools.Log.Println(turnServerTag, err)
			}
		} else if protocol == http.MethodConnect {

			tools.Log.Println(turnServerTag, "client want open seq", Id)
			s.mutex.RLock()
			call := s.pending[Id]
			s.mutex.RUnlock()

			if call != nil {
				call.Done <- conn
				<-call.Done
				tools.Log.Println(turnServerTag, "chan conn", "close")
			} else {
				http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
			}
		}
	} else {
		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
	}
}
